package com.spring.arch.common.exception.handler;


import com.spring.arch.common.dto.result.ErrorDetailDTO;
import com.spring.arch.common.dto.result.ResultDTO;
import com.spring.arch.common.exception.AppRuntimeException;
import com.spring.arch.common.exception.ServiceException;
import com.spring.arch.common.jackson.JsonMapper;
import com.spring.arch.common.utils.HttpUtils;
import com.spring.arch.common.dto.result.ErrorDetailDTO;
import com.spring.arch.common.dto.result.ResultDTO;
import com.spring.arch.common.exception.AppRuntimeException;
import com.spring.arch.common.exception.ServiceException;
import com.spring.arch.common.jackson.JsonMapper;
import com.spring.arch.common.utils.HttpUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.ConversionNotSupportedException;
import org.springframework.beans.TypeMismatchException;
import org.springframework.http.*;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.lang.Nullable;
import org.springframework.util.CollectionUtils;
import org.springframework.validation.BindException;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingPathVariableException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.context.request.async.AsyncRequestTimeoutException;
import org.springframework.web.multipart.support.MissingServletRequestPartException;
import org.springframework.web.servlet.NoHandlerFoundException;
import org.springframework.web.util.WebUtils;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
import java.util.Set;

/**
 * 重写ResponseEntityExceptionHandler，实现应用全局Exception处理器。<br>
 * JSON输出的内容ErrorMessage，延续Spring平台BasicErrorController的输出内容为基础。
 */
@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler({Exception.class})
    @Nullable
    public final ResponseEntity<Object> handleException(final Exception ex, final WebRequest request,
                                                        final HttpServletRequest httpServletRequest) {
        final HttpHeaders headers = new HttpHeaders();
        final String clientIp = HttpUtils.getClientIP(httpServletRequest);
        final String requestURI = httpServletRequest.getRequestURI();
        final String exClassName = ex.getClass().getName();
        try {
            final String errorMsg = ex.getLocalizedMessage();
            log.warn("[{}]请求处理异常: {}, {}, {}", clientIp, requestURI, exClassName, errorMsg, ex);
        } catch (Exception e) {
            log.warn("[{}]请求处理异常: {}, {}, {}", clientIp, requestURI, exClassName, "未知异常", ex);
        }
        /** 拷贝ResponseEntityExceptionHandler中的实现（类同于DefaultHandlerExceptionResolver） **/
        if (ex instanceof HttpRequestMethodNotSupportedException) {
            final HttpStatus status = HttpStatus.METHOD_NOT_ALLOWED;
            return this.handleHttpRequestMethodNotSupported((HttpRequestMethodNotSupportedException) ex, headers,
                    status, request);
        } else if (ex instanceof HttpMediaTypeNotSupportedException) {
            final HttpStatus status = HttpStatus.UNSUPPORTED_MEDIA_TYPE;
            return this.handleHttpMediaTypeNotSupported((HttpMediaTypeNotSupportedException) ex, headers, status,
                    request);
        } else if (ex instanceof HttpMediaTypeNotAcceptableException) {
            final HttpStatus status = HttpStatus.NOT_ACCEPTABLE;
            return this.handleHttpMediaTypeNotAcceptable((HttpMediaTypeNotAcceptableException) ex, headers, status,
                    request);
        } else if (ex instanceof MissingPathVariableException) {
            final HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
            return this.handleMissingPathVariable((MissingPathVariableException) ex, headers, status, request);
        } else if (ex instanceof MissingServletRequestParameterException) {
            final HttpStatus status = HttpStatus.BAD_REQUEST;
            return this.handleMissingServletRequestParameter((MissingServletRequestParameterException) ex, headers,
                    status, request);
        } else if (ex instanceof ServletRequestBindingException) {
            final HttpStatus status = HttpStatus.BAD_REQUEST;
            return this.handleServletRequestBindingException((ServletRequestBindingException) ex, headers, status,
                    request);
        } else if (ex instanceof ConversionNotSupportedException) {
            final HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
            return this.handleConversionNotSupported((ConversionNotSupportedException) ex, headers, status, request);
        } else if (ex instanceof TypeMismatchException) {
            final HttpStatus status = HttpStatus.BAD_REQUEST;
            return this.handleTypeMismatch((TypeMismatchException) ex, headers, status, request);
        } else if (ex instanceof HttpMessageNotReadableException) {
            final HttpStatus status = HttpStatus.BAD_REQUEST;
            return this.handleHttpMessageNotReadable((HttpMessageNotReadableException) ex, headers, status, request);
        } else if (ex instanceof HttpMessageNotWritableException) {
            final HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
            return this.handleHttpMessageNotWritable((HttpMessageNotWritableException) ex, headers, status, request);
        } else if (ex instanceof MethodArgumentNotValidException) {
            final HttpStatus status = HttpStatus.BAD_REQUEST;
            return this.handleMethodArgumentNotValid((MethodArgumentNotValidException) ex, headers, status, request);
        } else if (ex instanceof MissingServletRequestPartException) {
            final HttpStatus status = HttpStatus.BAD_REQUEST;
            return this.handleMissingServletRequestPart((MissingServletRequestPartException) ex, headers, status,
                    request);
        } else if (ex instanceof BindException) {
            final HttpStatus status = HttpStatus.BAD_REQUEST;
            return this.handleBindException((BindException) ex, headers, status, request);
        } else if (ex instanceof NoHandlerFoundException) {
            final HttpStatus status = HttpStatus.NOT_FOUND;
            return this.handleNoHandlerFoundException((NoHandlerFoundException) ex, headers, status, request);
        } else if (ex instanceof AsyncRequestTimeoutException) {
            final HttpStatus status = HttpStatus.SERVICE_UNAVAILABLE;
            return this.handleAsyncRequestTimeoutException((AsyncRequestTimeoutException) ex, headers, status, request);
        }
        /** 平台自定义实现的业务异常（基于RuntimeException） **/
        else if (ex instanceof AppRuntimeException) {
            final AppRuntimeException ae = (AppRuntimeException) ex;
            final HttpStatus status = ae.getStatus();

            final ResultDTO error = ResultDTO.failure(HttpStatus.BAD_REQUEST, ex.getMessage(),
                    new ErrorDetailDTO(String.valueOf(status.value()), status.getReasonPhrase() + ": " + ex.getMessage(), requestURI));
            return this.handleExceptionInternal(ex, error, new HttpHeaders(), status, request);
        }
        /** 平台自定义实现的业务异常（基于RuntimeException） **/
        else if (ex instanceof ServiceException) {
            final ServiceException se = (ServiceException) ex;
            final HttpStatus status = HttpStatus.BAD_REQUEST;

            final ResultDTO error = ResultDTO.failure(HttpStatus.BAD_REQUEST, ex.getMessage(),
                    new ErrorDetailDTO(String.valueOf(se.getCode()), ex.getMessage(), requestURI));
            return this.handleExceptionInternal(ex, error, new HttpHeaders(), status, request);
        }
        /** 未分类处理的异常，统一错误代码 **/
        else {
            return this.handleExceptionInternal(ex, null, new HttpHeaders(), HttpStatus.INTERNAL_SERVER_ERROR, request);
        }
    }

    /** 拷贝ResponseEntityExceptionHandler中的实现（类同于DefaultHandlerExceptionResolver） **/
    /**
     * Customize the response for HttpRequestMethodNotSupportedException.
     * <p>
     * This method logs a warning, sets the "Allow" header, and delegates to
     * {@link #handleExceptionInternal}.
     *
     * @param ex      the exception
     * @param headers the headers to be written to the response
     * @param status  the selected response status
     * @param request the current request
     * @return a {@code ResponseEntity} instance
     */
    protected ResponseEntity<Object> handleHttpRequestMethodNotSupported(
            final HttpRequestMethodNotSupportedException ex, final HttpHeaders headers, final HttpStatus status,
            final WebRequest request) {
        final Set<HttpMethod> supportedMethods = ex.getSupportedHttpMethods();
        if (!CollectionUtils.isEmpty(supportedMethods)) {
            headers.setAllow(supportedMethods);
        }
        return this.handleExceptionInternal(ex, null, headers, status, request);
    }

    /**
     * Customize the response for HttpMediaTypeNotSupportedException.
     * <p>
     * This method sets the "Accept" header and delegates to
     * {@link #handleExceptionInternal}.
     *
     * @param ex      the exception
     * @param headers the headers to be written to the response
     * @param status  the selected response status
     * @param request the current request
     * @return a {@code ResponseEntity} instance
     */
    protected ResponseEntity<Object> handleHttpMediaTypeNotSupported(
            final HttpMediaTypeNotSupportedException ex, final HttpHeaders headers, final HttpStatus status,
            final WebRequest request) {

        final List<MediaType> mediaTypes = ex.getSupportedMediaTypes();
        if (!CollectionUtils.isEmpty(mediaTypes)) {
            headers.setAccept(mediaTypes);
        }

        return this.handleExceptionInternal(ex, null, headers, status, request);
    }

    /**
     * Customize the response for HttpMediaTypeNotAcceptableException.
     * <p>
     * This method delegates to {@link #handleExceptionInternal}.
     *
     * @param ex      the exception
     * @param headers the headers to be written to the response
     * @param status  the selected response status
     * @param request the current request
     * @return a {@code ResponseEntity} instance
     */
    protected ResponseEntity<Object> handleHttpMediaTypeNotAcceptable(
            final HttpMediaTypeNotAcceptableException ex, final HttpHeaders headers, final HttpStatus status,
            final WebRequest request) {

        return this.handleExceptionInternal(ex, null, headers, status, request);
    }

    /**
     * Customize the response for MissingPathVariableException.
     * <p>
     * This method delegates to {@link #handleExceptionInternal}.
     *
     * @param ex      the exception
     * @param headers the headers to be written to the response
     * @param status  the selected response status
     * @param request the current request
     * @return a {@code ResponseEntity} instance
     * @since 4.2
     */
    protected ResponseEntity<Object> handleMissingPathVariable(
            final MissingPathVariableException ex, final HttpHeaders headers, final HttpStatus status,
            final WebRequest request) {

        return this.handleExceptionInternal(ex, null, headers, status, request);
    }

    /**
     * Customize the response for MissingServletRequestParameterException.
     * <p>
     * This method delegates to {@link #handleExceptionInternal}.
     *
     * @param ex      the exception
     * @param headers the headers to be written to the response
     * @param status  the selected response status
     * @param request the current request
     * @return a {@code ResponseEntity} instance
     */
    protected ResponseEntity<Object> handleMissingServletRequestParameter(
            final MissingServletRequestParameterException ex, final HttpHeaders headers, final HttpStatus status,
            final WebRequest request) {

        return this.handleExceptionInternal(ex, null, headers, status, request);
    }

    /**
     * Customize the response for ServletRequestBindingException.
     * <p>
     * This method delegates to {@link #handleExceptionInternal}.
     *
     * @param ex      the exception
     * @param headers the headers to be written to the response
     * @param status  the selected response status
     * @param request the current request
     * @return a {@code ResponseEntity} instance
     */
    protected ResponseEntity<Object> handleServletRequestBindingException(
            final ServletRequestBindingException ex, final HttpHeaders headers, final HttpStatus status,
            final WebRequest request) {

        return this.handleExceptionInternal(ex, null, headers, status, request);
    }

    /**
     * Customize the response for ConversionNotSupportedException.
     * <p>
     * This method delegates to {@link #handleExceptionInternal}.
     *
     * @param ex      the exception
     * @param headers the headers to be written to the response
     * @param status  the selected response status
     * @param request the current request
     * @return a {@code ResponseEntity} instance
     */
    protected ResponseEntity<Object> handleConversionNotSupported(
            final ConversionNotSupportedException ex, final HttpHeaders headers, final HttpStatus status,
            final WebRequest request) {

        return this.handleExceptionInternal(ex, null, headers, status, request);
    }

    /**
     * Customize the response for TypeMismatchException.
     * <p>
     * This method delegates to {@link #handleExceptionInternal}.
     *
     * @param ex      the exception
     * @param headers the headers to be written to the response
     * @param status  the selected response status
     * @param request the current request
     * @return a {@code ResponseEntity} instance
     */
    protected ResponseEntity<Object> handleTypeMismatch(
            final TypeMismatchException ex, final HttpHeaders headers, final HttpStatus status,
            final WebRequest request) {

        return this.handleExceptionInternal(ex, null, headers, status, request);
    }

    /**
     * Customize the response for HttpMessageNotReadableException.
     * <p>
     * This method delegates to {@link #handleExceptionInternal}.
     *
     * @param ex      the exception
     * @param headers the headers to be written to the response
     * @param status  the selected response status
     * @param request the current request
     * @return a {@code ResponseEntity} instance
     */
    protected ResponseEntity<Object> handleHttpMessageNotReadable(
            final HttpMessageNotReadableException ex, final HttpHeaders headers, final HttpStatus status,
            final WebRequest request) {

        return this.handleExceptionInternal(ex, null, headers, status, request);
    }

    /**
     * Customize the response for HttpMessageNotWritableException.
     * <p>
     * This method delegates to {@link #handleExceptionInternal}.
     *
     * @param ex      the exception
     * @param headers the headers to be written to the response
     * @param status  the selected response status
     * @param request the current request
     * @return a {@code ResponseEntity} instance
     */
    protected ResponseEntity<Object> handleHttpMessageNotWritable(
            final HttpMessageNotWritableException ex, final HttpHeaders headers, final HttpStatus status,
            final WebRequest request) {

        return this.handleExceptionInternal(ex, null, headers, status, request);
    }

    /**
     * Customize the response for MethodArgumentNotValidException.
     * <p>
     * This method delegates to {@link #handleExceptionInternal}.
     *
     * @param ex      the exception
     * @param headers the headers to be written to the response
     * @param status  the selected response status
     * @param request the current request
     * @return a {@code ResponseEntity} instance
     */
    protected ResponseEntity<Object> handleMethodArgumentNotValid(
            final MethodArgumentNotValidException ex, final HttpHeaders headers, final HttpStatus status,
            final WebRequest request) {

        return this.handleExceptionInternal(ex, null, headers, status, request);
    }

    /**
     * Customize the response for MissingServletRequestPartException.
     * <p>
     * This method delegates to {@link #handleExceptionInternal}.
     *
     * @param ex      the exception
     * @param headers the headers to be written to the response
     * @param status  the selected response status
     * @param request the current request
     * @return a {@code ResponseEntity} instance
     */
    protected ResponseEntity<Object> handleMissingServletRequestPart(
            final MissingServletRequestPartException ex, final HttpHeaders headers, final HttpStatus status,
            final WebRequest request) {

        return this.handleExceptionInternal(ex, null, headers, status, request);
    }

    /**
     * Customize the response for BindException.
     * <p>
     * This method delegates to {@link #handleExceptionInternal}.
     *
     * @param ex      the exception
     * @param headers the headers to be written to the response
     * @param status  the selected response status
     * @param request the current request
     * @return a {@code ResponseEntity} instance
     */
    protected ResponseEntity<Object> handleBindException(
            final BindException ex, final HttpHeaders headers, final HttpStatus status, final WebRequest request) {

        return this.handleExceptionInternal(ex, null, headers, status, request);
    }

    /**
     * Customize the response for NoHandlerFoundException.
     * <p>
     * This method delegates to {@link #handleExceptionInternal}.
     *
     * @param ex      the exception
     * @param headers the headers to be written to the response
     * @param status  the selected response status
     * @param request the current request
     * @return a {@code ResponseEntity} instance
     * @since 4.0
     */
    protected ResponseEntity<Object> handleNoHandlerFoundException(
            final NoHandlerFoundException ex, final HttpHeaders headers, final HttpStatus status,
            final WebRequest request) {

        return this.handleExceptionInternal(ex, null, headers, status, request);
    }

    /**
     * Customize the response for NoHandlerFoundException.
     * <p>
     * This method delegates to {@link #handleExceptionInternal}.
     *
     * @param ex         the exception
     * @param headers    the headers to be written to the response
     * @param status     the selected response status
     * @param webRequest the current request
     * @return a {@code ResponseEntity} instance
     * @since 4.2.8
     */
    @Nullable
    protected ResponseEntity<Object> handleAsyncRequestTimeoutException(
            final AsyncRequestTimeoutException ex, final HttpHeaders headers, final HttpStatus status,
            final WebRequest webRequest) {
        if (webRequest instanceof ServletWebRequest) {
            final ServletWebRequest servletWebRequest = (ServletWebRequest) webRequest;
            final HttpServletResponse response = servletWebRequest.getResponse();
            if ((response != null) && response.isCommitted()) {
                if (log.isWarnEnabled()) {
                    log.warn("Async request timed out");
                }
                return null;
            }
        }

        return this.handleExceptionInternal(ex, null, headers, status, webRequest);
    }

    /**
     * 拷贝ResponseEntityExceptionHandler中的缺省实现，对返回数据JSON化处理
     */
    protected ResponseEntity<Object> handleExceptionInternal(
            final Exception ex, @Nullable final Object body, final HttpHeaders headers, final HttpStatus status,
            final WebRequest webRequest) {
        Object bodyData = body;

        if (bodyData == null) {
            String field = (String) (webRequest.getAttribute(WebUtils.FORWARD_REQUEST_URI_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST));
            ErrorDetailDTO errorDetail;
            try {
                errorDetail = new ErrorDetailDTO(String.valueOf(status.value()), status.getReasonPhrase() + ": " + ex.getMessage(), field);
            } catch (Exception e) {
                errorDetail = new ErrorDetailDTO(String.valueOf(status.value()),ex.getClass().getName(), field);
            }
            bodyData = ResultDTO.failure(HttpStatus.INTERNAL_SERVER_ERROR, "未知异常，请联系管理人员", errorDetail);
        }

        log.info("异常栈信息>>>>>>>>>：" + JsonMapper.toJsonString(bodyData));

        webRequest.setAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE, ex,
                RequestAttributes.SCOPE_REQUEST);
        return new ResponseEntity<>(bodyData, headers, status);
    }
}